package wimon.net;

import java.io.IOException;
import java.net.*;
import java.nio.*;
import java.nio.channels.DatagramChannel;
import java.nio.charset.Charset;
import java.util.*;

import org.apache.commons.lang.StringUtils;

import edu.uci.ics.jung.graph.*;

public class MeshClient {

    NetworkInterface activeInterface;
    InetAddress activeAddress;
    InetAddress nodeAddress;
    Graph<InetAddress, Object> graph;

    private DatagramSocket socket;
    private DatagramChannel channel;
    private static int seq = 0;
    private InetAddress activeBroadcast;

    private static final int DAEMON_PORT = 22222;
    private static final int CLIENT_PORT = 22222;
    private static final int MSG_HEAD_SIZE = 12;
    private static final int MSG_BODY_SIZE = 1024;
    private static final int BYTES_PER_IN_ADDR = 4;
    private static final int DISCOVERY_TIME_OUT = 500; // millisecond
    private static final Charset DEFAULT_CHARSET = Charset.forName("US-ASCII");

    public MeshClient() throws SocketException, IOException {
	setActiveInterface(0);
	resetChannel();
	graph = new UndirectedSparseGraph<InetAddress, Object>();
    }

    public void resetChannel() throws SocketException, IOException {
	channel = DatagramChannel.open();
	socket = channel.socket();
	socket.bind(new InetSocketAddress(activeAddress, CLIENT_PORT));
	socket.setBroadcast(true);
    }

    public Graph<InetAddress, Object> getGraph() {
	return this.graph;
    }

    public void discover() throws IOException {
	// graph = new UndirectedSparseGraph<InetAddress, String>();

	Collection<InetAddress> vs = new ArrayList<InetAddress>(graph
		.getVertices());
	for (InetAddress a : vs)
	    graph.removeVertex(a);

	Thread t = new Thread(new Runnable() {

	    @Override
	    public void run() {

		byte[] body = activeAddress.getAddress();
		Message msg = new Message(Message.MSG_DISCOVER, body);

		try {
		    InetAddress address = activeBroadcast;
		    msg.send(address, DAEMON_PORT);
		} catch (Exception e) {
		    e.printStackTrace();
		}

		discoveryLoop: while (true) {

		    InetAddress a, b;

		    try {
			a = msg.receive();
		    } catch (IOException ioe) {
			/* timed out */
			break discoveryLoop;
		    }

		    if (msg.type != Message.MSG_DISC_RE)
			continue;

		    int n = msg.size - BYTES_PER_IN_ADDR;
		    byte[] bs = new byte[BYTES_PER_IN_ADDR];

		    System.arraycopy(msg.body, n, bs, 0, BYTES_PER_IN_ADDR);
		    try {
			a = InetAddress.getByAddress(bs);
		    } catch (UnknownHostException uhe) {
			uhe.printStackTrace();
		    }
		    b = null;
		    graph.addVertex(a);

		    while (n > BYTES_PER_IN_ADDR) {
			n -= BYTES_PER_IN_ADDR;
			System.arraycopy(msg.body, n, bs, 0, BYTES_PER_IN_ADDR);

			try {
			    b = InetAddress.getByAddress(bs);
			} catch (UnknownHostException uhe) {
			    uhe.printStackTrace();
			}

			if (!graph.containsVertex(b))
			    graph.addVertex(b);

			try {
			    // if(graph.findEdge(a, b) == null) {
			    graph.addEdge(new Object(), a, b);
			    System.out.println("added edge from a=" + a
				    + " to b=" + b);
			    // }
			} catch (Exception e) {
			    System.err.println("Graph Exception: "
				    + e.getMessage());
			}

			a = b;
		    }
		}
	    }
	});

	t.start();

	try {
	    t.join(DISCOVERY_TIME_OUT);
	} catch (InterruptedException e) {
	} finally {
	    t.interrupt();

	    System.out.println(graph);

	    resetChannel();
	}
    }

    public Message execute(String s) throws IOException {
	byte[] body = (s + '\0').getBytes();
	Message msg = new Message(Message.MSG_RUN, body);

	msg.send(nodeAddress, DAEMON_PORT);

	msg.receive();

	return msg;
    }

    public InetAddress getAddress() {
	return activeAddress;
    }

    public InetAddress[] getDiscoveredAddresses() {
	return graph.getVertices().toArray(new InetAddress[0]);
    }

    public void setNodeAddress(final InetAddress address) {
	this.nodeAddress = address;
    }

    public void setActiveInterface(int index) throws SocketException {
	activeInterface = MeshClient.getInterfaces()[index];
	activeAddress = activeInterface.getInetAddresses().nextElement();

	try {
	    activeBroadcast = InetAddress.getByName("255.255.255.255");
	} catch (Exception e) {
	    e.printStackTrace();
	}
    }

    public static NetworkInterface[] getInterfaces() throws SocketException {
	Enumeration<NetworkInterface> ifs = NetworkInterface
		.getNetworkInterfaces();
	ArrayList<NetworkInterface> res = new ArrayList<NetworkInterface>();

	while (ifs.hasMoreElements()) {
	    NetworkInterface ni = (NetworkInterface) ifs.nextElement();
	    if (!ni.isLoopback() && !ni.isVirtual() && ni.isUp())
		res.add(ni);
	}

	return res.toArray(new NetworkInterface[0]);
    }

    private static int nextSeq() {
	return seq++;
    }

    public class Message {
	final static int MSG_DISCOVER = 1;
	final static int MSG_RUN = 3;

	final static int MSG_DISC_RE = 129;
	final static int MSG_RUN_RE = 130;

	int type;
	int seq;
	int size;
	byte[] body;

	Message(int type, byte[] body) {
	    this.type = type;
	    this.size = Math.min(body.length, MSG_BODY_SIZE);
	    this.seq = MeshClient.nextSeq();
	    this.body = Arrays.copyOf(body, this.size);
	}

	public String getBodyAsString() {
	    return StringUtils.substringBefore(new String(body, 0, body.length,
		    DEFAULT_CHARSET), "\0");
	}

	void send(SocketAddress sa) throws IOException {
	    int messageSize = MSG_HEAD_SIZE + body.length;
	    java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocate(messageSize);
	    bb.order(ByteOrder.LITTLE_ENDIAN);
	    bb.putInt(this.type);
	    bb.putInt(this.seq);
	    bb.putInt(this.size);
	    bb.put(this.body);

	    bb.flip();
	    channel.send(bb, sa);
	}

	void send(InetAddress address, int port) throws IOException {
	    this.send(new InetSocketAddress(address, port));
	}

	InetAddress receive() throws IOException {
	    int messageSize = MSG_HEAD_SIZE + MSG_BODY_SIZE;
	    ByteBuffer bb = ByteBuffer.allocate(messageSize);
	    bb.order(ByteOrder.LITTLE_ENDIAN);

	    InetSocketAddress sa = (InetSocketAddress) channel.receive(bb);
	    bb.flip();

	    this.type = bb.getInt();
	    this.seq = bb.getInt();
	    this.size = bb.getInt();
	    this.body = new byte[this.size];
	    bb.get(this.body);

	    return sa.getAddress();
	}

	public String toString() {
	    String bs = new String(this.body, MSG_HEAD_SIZE, Math.min(16,
		    this.body.length), DEFAULT_CHARSET);
	    return String.format("[type=%d, seq=%d, size=%d, body(<16)='%s']",
		    this.type, this.seq, this.size, bs);
	}
    }
}
